Notebook de nettoyage

Adrian Rodriguez

Projet 5 parcours Ingénieur Machine Learning

Accès projet git : https://github.com/Adrian1903/Categorisez-automatiquement-des-questions
Plus d'informations : https://openclassrooms.com/fr/paths/148-ingenieur-machine-learning


0. Contexte

Stack Overflow est un site célèbre de questions-réponses liées au développement informatique. Pour poser une question sur ce site, il faut entrer plusieurs tags de manière à retrouver facilement la question par la suite. Pour les utilisateurs expérimentés, cela ne pose pas de problème, mais pour les nouveaux utilisateurs, il serait judicieux de suggérer quelques tags relatifs à la question posée.

Amateur de Stack Overflow, qui vous a souvent sauvé la mise, vous décidez d'aider la communauté en retour. Pour cela, vous développez un système de suggestion de tag pour le site. Celui-ci prendra la forme d’un algorithme de machine learning qui assigne automatiquement plusieurs tags pertinents à une question.

Les données Stack Overflow propose un outil d’export de données - "stackexchange explorer", qui recense un grand nombre de données authentiques de la plateforme d’entraide.

Contraintes :

  • Mettre en œuvre une approche non supervisée.
  • Utiliser une approche supervisée ou non pour extraire des tags à partir des résultats précédents.
  • Comparer ses résultats à une approche purement supervisée, après avoir appliqué des méthodes d’extraction de features spécifiques des données textuelles.
  • Mettre en place une méthode d’évaluation propre, avec une séparation du jeu de données pour l’évaluation.
  • Pour suivre les modifications du code final à déployer, utiliser un logiciel de gestion de versions, par exemple Git.
In [1]:
import pandas as pd
pd.options.display.max_columns = None


from collections import Counter

from IPython.display import Image

import seaborn as sns
import matplotlib.pyplot as plt
plt.style.use("default")

from wordcloud import WordCloud
import PIL.Image
import en_core_web_sm
import en_core_web_md
import spacy
from spacy.lang.en.stop_words import STOP_WORDS


from functions import *
In [2]:
start_notebook = time.time()

1. Importation et exploration préliminaire

Les données ont été extraite du site stackexchangeexplorer. J'ai traité des post récents.
Requête SQL :

DECLARE @max_date as DATETIME = DATEADD(MONTH, -1, GETDATE())

SELECT Id, Title, Body, Tags    
FROM posts
WHERE CreationDate < @max_date AND PostTypeId = 1 AND Score > 19
ORDER BY CreationDate DESC

Les données ont été stockées dans le fichier 'Questions50K_QueryResults.csv'.

In [3]:
questions_raw = pd.read_csv('src/Questions50K_QueryResults.csv')
In [4]:
questions_raw.head()
Out[4]:
Id Title Body Tags
0 63087217 Changes using mutable reference of a field are... <p>I was trying to manipulate the field <code>... <rust><reference><move>
1 63078532 Is it possible to create a new data type in Ja... <p>Is it possible to create a new data type in... <javascript>
2 63067062 Elastic Search indexes gets deleted frequently <p>I'm running an elastic search for a persona... <elasticsearch>
3 63046397 Why does this usage of C++17 if constexpr fail? <p>I am trying to use C++17 <code>if constexpr... <c++><if-statement><templates><c++17><constexpr>
4 63043585 VSCode showing "Java 11 or more recent is requ... <p>VSCode started showing me today a pop-up sa... <java><visual-studio-code>
In [5]:
questions_raw.shape
Out[5]:
(50000, 4)
In [6]:
questions_raw.columns
Out[6]:
Index(['Id', 'Title', 'Body', 'Tags'], dtype='object')
In [7]:
questions_raw.isna().sum()
Out[7]:
Id       0
Title    0
Body     0
Tags     0
dtype: int64
In [8]:
questions = questions_raw.copy()

2. Traitement des tags

Après l'exploration des tags, je vais en premier lieu définir ceux à conserver selon la loi de pareto, puis supprimer les autres du jeu de données. Les questions se retrouvant sans tags seront supprimées.

2-1. Exploration

Je cherche à connaître quels sont les tags les plus utilisés

In [9]:
questions.Tags = questions.Tags.apply(lambda x: x.replace('<', '').replace('>', ',')).str.slice(0, -1).str.replace('-', '_')
questions.Tags
Out[9]:
0                               rust,reference,move
1                                        javascript
2                                     elasticsearch
3        c++,if_statement,templates,c++17,constexpr
4                           java,visual_studio_code
                            ...                    
49995       ios,service_worker,progressive_web_apps
49996                javascript,jquery,html,ios,css
49997                       javascript,getselection
49998               ios,permissions,ios_permissions
49999                                           sql
Name: Tags, Length: 50000, dtype: object
In [10]:
tags = questions.Tags.str.split(',').explode().reset_index()
tags = tags.groupby('Tags').count().sort_values(by='index', ascending=False).reset_index().rename(columns={'index': 'recurrence'})
print(f'Nous avons {len(tags)} tags utilisés sur la période')
tags
Nous avons 10573 tags utilisés sur la période
Out[10]:
Tags recurrence
0 javascript 5266
1 python 5206
2 android 5113
3 java 3426
4 angular 2860
... ... ...
10568 jmxtrans 1
10569 jms_topic 1
10570 jms 1
10571 jmod 1
10572 zurb_foundation_6 1

10573 rows × 2 columns

In [11]:
text = (tags.Tags + ' ') * tags['recurrence']
text = ''.join(text)

# Création de l'objet
mask = np.array(PIL.Image.open('src/bulle.jpg'))
wordcloud = WordCloud(width=1200, height=780, background_color="rgba(255, 255, 255, 0)", mode="RGBA", collocations=False, mask=mask).generate(text)

# Génération de l'image
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.margins(x=0, y=0)

plt.savefig('img/img_wordcloud_tags.png',
            dpi=500, quality=95,
            transparent=True,
            bbox_inches="tight")
plt.show()
In [12]:
# Nombre de tags utilisés par question
tags_count = questions.Tags.str.split(',').apply(lambda x: len(x)).reset_index()
tags_count = tags_count.groupby('Tags').count().reset_index()

plt.clf()
sns.barplot(y='Tags',
            x='index',
            data=tags_count,
            orient='h')
plt.title('Nombre de tags utilisés par question')
plt.xlabel('Volume de post')
plt.ylabel('Nombre de tags')

plt.savefig('img/img_tags_count.png',
            transparent=True,
            bbox_inches="tight")

2-2. Sélection des tags à conserver

In [13]:
# Calcul de l'importance des tags
tags_imp = (tags['recurrence'].cumsum() / tags['recurrence'].sum()) * 100
tags_imp = tags_imp.reset_index().rename(columns={'recurrence': 'cumsum_percent'})

vol_treshold = 80
qty_tags = len(tags_imp[tags_imp.cumsum_percent < vol_treshold])
vol_qty_tags = round(qty_tags / len(tags_imp) * 100, 0)
print(f'{vol_treshold} % du volume des tags est expliqué par {qty_tags} tags soit une proportion de {vol_qty_tags} % de la population des tags')

sns.lineplot(x='index', y='cumsum_percent', data=tags_imp)
plt.axhline(y=vol_treshold, color='red', linestyle='--')
plt.title("Somme cumulée d'utilisation des tags")
plt.ylabel('Somme cumulée')
plt.xlabel('Index des tags')
x = range(len(tags))
y1 = vol_treshold
plt.fill_between(x, y1, 100, color="red", alpha=0.3)
plt.annotate("Seuil : " + str(vol_treshold) + " %", (len(tags) / 2, vol_treshold - 5), color="red")

plt.savefig('img/img_pareto_tag.png',
            transparent=True,
            bbox_inches="tight")
plt.show()
80 % du volume des tags est expliqué par 969 tags soit une proportion de 9.0 % de la population des tags
In [14]:
bag_tags = tags.head(qty_tags)
bag_tags
Out[14]:
Tags recurrence
0 javascript 5266
1 python 5206
2 android 5113
3 java 3426
4 angular 2860
... ... ...
964 winapi 20
965 git_rebase 20
966 angularfire2 20
967 gruntjs 20
968 statusbar 20

969 rows × 2 columns

Les tags les moins souvent utilisés l'ont été au moins 20 fois sur des sujets reconnus et pertinents.

In [15]:
# Constitution d'une liste
bag_tags_lst = bag_tags.Tags.to_list()

2-3. Tags inclus dans les stopwords et protection de ponctuation

Je dois faire attention que certains tags ne soient pas également des stopwords, car il seront protégés. je dois les retirer

In [16]:
[display(bag_tags[bag_tags.Tags == i]) for i in bag_tags_lst if i in STOP_WORDS]
Tags recurrence
61 go 366
Tags recurrence
729 this 27
Out[16]:
[None, None]
In [17]:
bag_tags_lst.remove('go')
bag_tags_lst.remove('this')
In [18]:
display('go' in bag_tags_lst)
display('this' in bag_tags_lst)
False
False
In [19]:
# Recherche de la ponctuation présente dans les tags
punct = re.sub('[\w\s]', '', ' '.join(bag_tags_lst))
punct = repr(set([p for p in punct]))
print(f'Les ponctutations ' + punct + ' seront protégées en cas de suppression de ponctuation')
Les ponctutations {'.', '#', '+'} seront protégées en cas de suppression de ponctuation

Spoil : Enlever les ponctuations diminue la précision du modèle final. A vouloir trop nettoyer, je perds en précision. je laisse les ponctuations pour ce nettoyage.

2-4. Nettoyage des tags non sélectionnés

In [20]:
questions.Tags.head(20)
Out[20]:
0                                   rust,reference,move
1                                            javascript
2                                         elasticsearch
3            c++,if_statement,templates,c++17,constexpr
4                               java,visual_studio_code
5     c++,performance,x86_64,cpu_architecture,intege...
6     python,python_3.x,discord,discord.py,discord.p...
7                        c#,powershell,.net_core,bigint
8     java,casting,type_conversion,implicit_conversi...
9     google_chrome,xpath,google_chrome_devtools,ins...
10           javascript,google_chrome,web,download,xlsx
11             java,java_11,java_platform_module_system
12                                            c++,c++20
13      javascript,google_chrome,google_chrome_devtools
14             python,syntax_error,comparison_operators
15           c++,gcc,g++,compiler_optimization,volatile
16                          reactjs,javascript_debugger
17                                        python,pandas
18                                 c++,class,c++11,auto
19                             c++,lambda,c++20,stdbind
Name: Tags, dtype: object
In [21]:
i = 0
for sub in questions.Tags.str.split(','):
    lst = []
    for s_sub in sub:
        if s_sub in bag_tags_lst:
           lst.append(s_sub)
    questions.Tags[i] = ','.join(lst)
    i += 1

questions.Tags.head(20)
Out[21]:
0                                       rust,reference
1                                           javascript
2                                        elasticsearch
3           c++,if_statement,templates,c++17,constexpr
4                              java,visual_studio_code
5                               c++,performance,x86_64
6                                    python,python_3.x
7                              c#,powershell,.net_core
8     java,casting,type_conversion,implicit_conversion
9                 google_chrome,google_chrome_devtools
10               javascript,google_chrome,web,download
11                                        java,java_11
12                                           c++,c++20
13     javascript,google_chrome,google_chrome_devtools
14                                 python,syntax_error
15                   c++,gcc,g++,compiler_optimization
16                                             reactjs
17                                       python,pandas
18                                c++,class,c++11,auto
19                                    c++,lambda,c++20
Name: Tags, dtype: object
In [22]:
tags = questions.Tags.str.split(',').explode().reset_index()
tags = tags.groupby('Tags').count().sort_values(by='index', ascending=False).reset_index().rename(columns={'index': 'recurrence'})
print(f'Nous avons {len(tags)} tags utilisés sur la période')
tags
Nous avons 968 tags utilisés sur la période
Out[22]:
Tags recurrence
0 javascript 5266
1 python 5206
2 android 5113
3 java 3426
4 angular 2860
... ... ...
963 winapi 20
964 statusbar 20
965 httpclient 20
966 implicit_conversion 20
967 telegram 20

968 rows × 2 columns

In [23]:
# Suppresion des questions sans tags
questions[questions.Tags == '']
Out[23]:
Id Title Body Tags
196 60790801 Azure SQL DB Error, This location is not avail... <p>I am having pay as you go subscription and ...
217 60559934 ERROR in ngcc is already running at process wi... <p>When i try to run ng serve command in my pr...
250 60306645 Why do I get this GRPC Error "WARNING: Emulato... <p>Good evening everyone, I have only been dea...
380 59508137 "Requests from referer https://www.googleapis.... <p>I'm Brazilian, and I'm starting to make a s...
497 58956527 Migrating .NET Core 2 to .NET Core 3: HttpCont... <p>I am following this guide <a href="https://...
... ... ... ... ...
49645 29990745 How do you extract local variable information ... <p>My goal is:</p>\n\n<ul>\n<li>Given a suspen...
49829 29947836 DotCover: How to remove code coverage highligh... <p>I am using JetBrains' <a href="https://www....
49984 29900253 Debug application which is run using pm2 <p>Application is run by</p>\n\n<pre><code>pm2...
49985 29900130 What's Ramda equivalent to underscore.js 'comp... <p>Does Ramda have a function to remove false ...
49993 29897684 Is LDAP DN case insensitive? <p>I build some feature that assumes that LDAP...

866 rows × 4 columns

On se retrouve avec des questions sans tags. Je les retire.

In [24]:
questions = questions[questions.Tags != '']
In [25]:
# Backup de la liste de tags pour la production
pd.DataFrame(bag_tags_lst, columns=['list']).to_csv('api/src/bag_tags.csv', index=False)

3. Traitement des questions

3-1. Exploration des champs texte

En procédant à un test maison, je me rends facilement compte que je peux mettre des informations importante soit dans le titre, soit dans le body. Je ne pense pas forcément à les mettre dans les 2 blocs. De ce fait, je dois analyser le champs Body et le champs Title.

In [26]:
Image('src/post.png')
Out[26]:
In [27]:
questions['Title_Body'] = questions.Title + ' ' + questions.Body
questions
Out[27]:
Id Title Body Tags Title_Body
0 63087217 Changes using mutable reference of a field are... <p>I was trying to manipulate the field <code>... rust,reference Changes using mutable reference of a field are...
1 63078532 Is it possible to create a new data type in Ja... <p>Is it possible to create a new data type in... javascript Is it possible to create a new data type in Ja...
2 63067062 Elastic Search indexes gets deleted frequently <p>I'm running an elastic search for a persona... elasticsearch Elastic Search indexes gets deleted frequently...
3 63046397 Why does this usage of C++17 if constexpr fail? <p>I am trying to use C++17 <code>if constexpr... c++,if_statement,templates,c++17,constexpr Why does this usage of C++17 if constexpr fail...
4 63043585 VSCode showing "Java 11 or more recent is requ... <p>VSCode started showing me today a pop-up sa... java,visual_studio_code VSCode showing "Java 11 or more recent is requ...
... ... ... ... ... ...
49995 29895387 Service workers and iOS / Safari <p>On Chromium's page about service workers th... ios,service_worker,progressive_web_apps Service workers and iOS / Safari <p>On Chromiu...
49996 29894997 Prevent iOS bounce without disabling scroll ab... <p>I am trying to implement a solution to prev... javascript,jquery,html,ios,css Prevent iOS bounce without disabling scroll ab...
49997 29894781 Javascript selected text highlighting prob <p>I have a html page with text content. On se... javascript Javascript selected text highlighting prob <p>...
49998 29894749 Complete list of iOS app permissions <p>Different web sites (<a href="http://www.ho... ios,permissions Complete list of iOS app permissions <p>Differ...
49999 29894645 How to skip the first n rows in sql query <p>I want to fire a Query "<code>SELECT * FROM... sql How to skip the first n rows in sql query <p>I...

49134 rows × 5 columns

3-2. Nettoyage du texte

Pour y arriver, je vais m'aider du pipeline sPacy. Pour ce projet, je n'ai pas besoin de la reconnaissance de nom (NER), ce composant sera désactivé pour des économies de calcul.

3-2-1. Traitement avant taggage POS

In [28]:
Image('src/question.png')
Out[28]:

Lorsque j'analyse une question. Je constate qu'il peut y avoir des blocs de codes, et des blocs d'image. Ces blocs génèrent du bruit et n'apporte pas de ou très peu de valeurs ajoutées au sens de la question. Je décide de les retirer. Les blocs de code non préformaté ont très peu d'incidence et contiennent souvent des tags. Je les conserve.

Je retire également les tags HTML, les retours ligne \n, les éventuels caractères accentués et je passe tous les caractères en minuscule s'il ne le sont pas déjà.

Je constate aussi qu'il peut y avoir des formes contractées des verbes. Ces verbes peuvent être important pour le sens de la phrase. La ponctutation est aussi génante, ca peut être constitué comme un token. Une ponctuation peut également être liées à un mot. Je vais uniformiser tout cela.

In [29]:
nlp = spacy.load('en_core_web_sm', disable=['ner'])

nlp.add_pipe(CleanBeforeTaggerComponent(nlp), first=True)
nlp.add_pipe(CleanContractionsComponent(nlp),
             after='CleanBeforeTagger')

print('Pipeline:', nlp.pipe_names)
Pipeline: ['CleanBeforeTagger', 'CleanContractions', 'tagger', 'parser']

3-2-2. Traitement après parser (.dep_)

In [30]:
questions.Title_Body[3]
Out[30]:
'Why does this usage of C++17 if constexpr fail? <p>I am trying to use C++17 <code>if constexpr</code> for conditional compilation, but it does not behave the way I expect.</p>\n<p>For example, with the code below, C++ still compiles the code defined by the macro <code>X2</code>,</p>\n<pre><code>#include &lt;map&gt;\n#include &lt;string&gt;\n#include &lt;iostream&gt;\n#include &lt;type_traits&gt;\n\n#define X1 pp(&quot;x&quot;)\n#define X2 pp(&quot;x&quot;, &quot;x&quot;)\n\nvoid pp(const std::string str)\n{\n   std::cout &lt;&lt; str &lt;&lt; std::endl;\n}\n\nint main()\n{\n   std::map&lt;std::string, int&gt; map;\n\n   if constexpr (std::is_null_pointer_v&lt;decltype(map)&gt;)\n      X2;\n   else\n      X1;\n}\n</code></pre>\n<p>and spits out this error messages:</p>\n<pre class="lang-none prettyprint-override"><code>1.c:6:23: error: too many arguments to function ‘void pp(std::__cxx11::string)’\n #define X2 pp(&quot;x&quot;, &quot;x&quot;)\n                       ^\n1.c:18:3: note: in expansion of macro ‘X2’\n   X2;\n   ^~\n</code></pre>\n<p>How can I skip compilation of X2?</p>\n'
In [31]:
doc = nlp(questions.Title_Body[3])
In [32]:
for token in doc:
    print(token.text, token.pos_, token.dep_, token.head)
why ADV ROOT why
does AUX aux why
this DET det usage
usage NOUN dobj does
of ADP prep usage
c++17 NOUN pobj of
if SCONJ mark fail
constexpr NOUN advmod fail
fail VERB advcl does
? PUNCT punct does
i PRON nsubj trying
am AUX aux trying
trying VERB ROOT trying
to PART aux use
use VERB xcomp trying
c++17 NOUN dobj use
if SCONJ mark constexpr
constexpr NOUN advcl use
for ADP prep use
conditional ADJ amod compilation
compilation NOUN pobj for
, PUNCT punct trying
but CCONJ cc trying
it PRON nsubj behave
does AUX aux behave
not PART neg behave
behave VERB conj trying
the DET det way
way NOUN dobj behave
i PRON nsubj expect.for
expect.for ADP relcl way
example NOUN dobj expect.for
, PUNCT punct behave
with ADP prep behave
the DET det code
code NOUN pobj with
below ADV advmod code
, PUNCT punct compiles
c++ PROPN nsubj compiles
still ADV advmod compiles
compiles VERB conj behave
the DET det code
code NOUN dobj compiles
defined VERB acl code
by ADP agent defined
the DET det x2,and
macro ADJ compound x2,and
x2,and PROPN pobj by
spits VERB conj behave
out ADP prt spits
this DET det messages
error NOUN compound messages
messages NOUN dobj spits
: PUNCT punct messages
how ADV advmod skip
can AUX aux skip
i PRON nsubj skip
skip VERB acl messages
compilation NOUN dobj skip
of ADP prep compilation
x2 PROPN pobj of
? PUNCT punct behave

Les mots qui sont présent à la racine (ROOT) d'une phrase sont très importants. Ils donnent le sens à la question. De ce fait je dois les conserver. De même pour les noms (NOUN). Je conserve également les mots qui sont présents dans la liste de tags.

Je conserve les versions lemmatisées des mots.

Cette structure me permet de garder un sens à la phrase et les principaux mots-clés.

In [33]:
full_txt = [token.text for token in doc]
txt = [token.lemma_ for token in doc
        if ((token.dep_ == 'ROOT' or
        token.pos_ == 'NOUN' or
        token.pos_ == 'ADJ' or
        token.pos_ == 'ADV') and
        token.text not in STOP_WORDS) or
        token.text in bag_tags_lst]

full_txt = ' '.join(full_txt)
txt = ' '.join(txt)

print(f'Texte avant nettoyage : \n{full_txt}\n\nTexte après nettoyage : \n{txt}')
Texte avant nettoyage : 
why does this usage of c++17 if constexpr fail ? i am trying to use c++17 if constexpr for conditional compilation , but it does not behave the way i expect.for example , with the code below , c++ still compiles the code defined by the macro x2,and spits out this error messages : how can i skip compilation of x2 ?

Texte après nettoyage : 
usage c++17 constexpr try c++17 constexpr conditional compilation way example code c++ code macro error message compilation
In [34]:
clean = CleanAfterParserComponent(nlp)
clean.set_protect(bag_tags_lst)
nlp.add_pipe(clean, after='parser')

print('Pipeline:', nlp.pipe_names)
Pipeline: ['CleanBeforeTagger', 'CleanContractions', 'tagger', 'parser', 'CleanAfterParser']

On ne fait pas de stemmization (racine des mots) dans ce projet. Dans le cas suivant, access peut être est un tag (microsoft access). Si on prend la racine de la plupart des mots suivants, on se retrouvera avec des faux tags dans le texte.

'access', 'access_token', 'accessi', 'accessibility', 'accessibilitybundle', 'accessibilityinfo', 'accessible', 'accessor', 'accessorily', 'accessory', 'accessright',

3-2-3. Exécution du nettoyage

In [35]:
%time questions['Cleaned_Title_Body'] = questions.Title_Body.apply(lambda x: nlp(x)).astype(str)
Wall time: 26min 23s

3-2-4. Vérification

In [36]:
display(questions_raw['Title'][0])
display(questions_raw['Body'][0])
'Changes using mutable reference of a field are not reflected after move of the original instance'
'<p>I was trying to manipulate the field <code>x</code> of the struct <code>Foo</code> by borrowing a mutable reference from its instance <code>foo</code>.</p>\n<p>If I try to print the field <code>x</code> using the moved binding <code>y</code> of the instance <code>foo</code> <strong>after</strong> the move of the original instance, it keeps printing the value that haven\'t changed.</p>\n<p>Simplified example below:</p>\n<pre class="lang-rust prettyprint-override"><code>struct Foo {\n    x: i32,\n}\n\nfn main() {\n    let mut foo = Foo { x: 42 };\n    let x = &amp;mut foo.x;\n    *x = 13;\n    let y = foo;\n    println!(&quot;{}&quot;, y.x); // -&gt; 42; expected result: 13\n}\n</code></pre>\n<p>Instead, if I print the moved binding <code>y</code> itself, it prints the changed value.</p>\n<pre class="lang-rust prettyprint-override"><code>println!(&quot;{:?}&quot;, y); // -&gt; Foo { x: 13 }\n</code></pre>\n<p>Or, if I print something else like <code>x</code> or <code>foo.x</code> <strong>before</strong> the move, it prints the thing as expected.</p>\n<pre class="lang-rust prettyprint-override"><code>println!(&quot;{}&quot;, x); // -&gt; 13\nlet y = foo;\nprintln!(&quot;{}&quot;, y.x); // -&gt; 13\n</code></pre>\n<p>Is this an intended behavior?</p>\n'
In [37]:
questions.Cleaned_Title_Body[0]
Out[37]:
'change mutable reference field reflect original instance field x struct foo mutable reference instance field x y instance foo original instance keep print value example instead print foo.x thing intended behavior'
In [38]:
text = ' '.join(questions.Cleaned_Title_Body)
# Création de l'objet
mask = np.array(PIL.Image.open('src/cloud.png'))
wordcloud = WordCloud(width=1200,
                      height=780,
                      background_color="rgba(255, 255, 255, 0)",
                      mode="RGBA",
                      collocations=False,
                      mask=mask).generate(text)
 
# Génération de l'image
plt.imshow(wordcloud, interpolation='bilinear')
plt.axis("off")
plt.margins(x=0, y=0)

plt.savefig('img/img_wordcloud_tokens.png',
            dpi=500, quality=95,
            transparent=True,
            bbox_inches="tight")

3-3. Sélection des mots à conserver après nettoyage du texte

Comme pour la sélection de tags, je vais appliquer un loi de pareto aux mots. Car beaucoup ne paraissent qu'une fois dans l'ensemble du jeu de donnée et ne seront pas utile au modèle. Ils ne feront qu'augmenter le temps de calcul. De même, certains parraisent très souvent et n'apportera aucune valeur au modèle, car très généraliste. Ces derniers pourrait être intégrer à des stopwords "métiers"

In [39]:
nlp_word = pd.DataFrame(text.split(' '), columns=['word']).reset_index()
nlp_word = nlp_word.groupby('word').count().sort_values(by='index', ascending=False).reset_index().rename(columns={'index': 'recurrence'})
nlp_word
Out[39]:
word recurrence
0 error 20339
1 code 18353
2 file 17608
3 way 13485
4 function 10187
... ... ...
90316 functioneverything 1
90317 functioneven 1
90318 functiondeclaration 1
90319 functionconsider 1
90320 1

90321 rows × 2 columns

In [40]:
# Calcul de l'importance des mots
word_imp = (nlp_word['recurrence'].cumsum() / nlp_word['recurrence'].sum()) * 100
word_imp = word_imp.reset_index().rename(columns={'recurrence': 'cumsum_percent'})

vol_treshold_up = 100
vol_treshold_down = 0

qty_words_up = len(word_imp[word_imp.cumsum_percent < vol_treshold_up])
vol_qty_words = round(qty_words_up / len(word_imp) * 100, 0)
print(f'{vol_treshold_up} % du volume des mots est expliqué par {qty_words_up} mots soit une proportion de {vol_qty_words} % de la population des mots')

qty_words_down = len(word_imp[word_imp.cumsum_percent < vol_treshold_down])
vol_qty_words_down = round(qty_words_down / len(word_imp) * 100, 0)
print(f'{vol_treshold_down} % du volume des mots est expliqué par {qty_words_down} mots soit une proportion de {vol_qty_words_down} % de la population des mots')

sns.lineplot(x='index', y='cumsum_percent', data=word_imp)
plt.axhline(y=vol_treshold_up, color='red', linestyle='--')
plt.axhline(y=vol_treshold_down, color='red', linestyle='--')
plt.title("Somme cumulée d'utilisation des mots")
plt.ylabel('Somme cumulée')
plt.xlabel('Index des mots')
x = range(len(nlp_word))
y1 = vol_treshold_down
y2 = vol_treshold_up
plt.fill_between(x, y1, color="red", alpha=0.3)
plt.fill_between(x, y2, 100, color='red', alpha=0.3)
plt.annotate("Seuil : " + str(vol_treshold_up) + " %", (len(nlp_word) / 2, vol_treshold_up - 5), color="red")
plt.annotate("Seuil : " + str(vol_treshold_down) + " %", (len(nlp_word) / 2, vol_treshold_down + 2), color="red")

plt.savefig('img/img_pareto_words.png',
            dpi=500, quality=95,
            transparent=True,
            bbox_inches="tight")
plt.show()
100 % du volume des mots est expliqué par 90320 mots soit une proportion de 100.0 % de la population des mots
0 % du volume des mots est expliqué par 0 mots soit une proportion de 0.0 % de la population des mots
In [41]:
bag_words = nlp_word.copy()
In [42]:
min_rec_word = bag_words.iloc[qty_words_up].recurrence
print(f'je conserve les mots qui sont utilisés au moins {min_rec_word} fois')
bag_words = bag_words[bag_words.recurrence >= min_rec_word]
bag_words.tail(10)
je conserve les mots qui sont utilisés au moins 1 fois
Out[42]:
word recurrence
90311 functiongoing 1
90312 functionfound 1
90313 functioncomponent 1
90314 functionfeedentrydao.java 1
90315 functionexpression 1
90316 functioneverything 1
90317 functioneven 1
90318 functiondeclaration 1
90319 functionconsider 1
90320 1
In [43]:
max_rec_word = bag_words.iloc[qty_words_down].recurrence
print(f'je supprime les mots qui sont utilisés plus de {max_rec_word} fois')
bag_words = bag_words[bag_words.recurrence <= max_rec_word]
bag_words.head(10)
je supprime les mots qui sont utilisés plus de 20339 fois
Out[43]:
word recurrence
0 error 20339
1 code 18353
2 file 17608
3 way 13485
4 function 10187
5 try 9331
6 app 9308
7 new 9060
8 project 8936
9 value 8905

3-4. Nettoyage des mots non sélectionnés

In [44]:
# Je rejette les mots non conservé 
if len(bag_words) != len(nlp_word):
    print('Rejet des mots en cours')
    for r in range(4): 
    # J'ai besoin d'éxécuter plusieurs fois pour que l'ensemble des mots à rejeter le soit
        i = 0
        print(f'Boucle {r}')
        for sub in questions.Cleaned_Title_Body.str.split():
            lst = []
            for s_sub in sub:
                if s_sub in bag_words.to_list():
                    lst.append(s_sub)
            questions.Cleaned_Title_Body[i] = ' '.join(lst)
            i += 1
            
    # Ne pas supprimer | Il y a une différence entre les 2 formulations
    questions['Cleaned_Title_Body'] = questions.Cleaned_Title_Body
else: print('Aucun mot à rejeter')
Aucun mot à rejeter
In [45]:
# Vérification
cleaned_nlp_word = ' '.join(questions.Cleaned_Title_Body)

cleaned_nlp_word = pd.DataFrame(cleaned_nlp_word.split(' '), columns=['word']).reset_index()
cleaned_nlp_word = cleaned_nlp_word.groupby('word').count().sort_values(by='index', ascending=False).reset_index().rename(columns={'index': 'recurrence'})
cleaned_nlp_word
Out[45]:
word recurrence
0 error 20339
1 code 18353
2 file 17608
3 way 13485
4 function 10187
... ... ...
90316 functioneverything 1
90317 functioneven 1
90318 functiondeclaration 1
90319 functionconsider 1
90320 1

90321 rows × 2 columns

In [46]:
# Phrases ne contenant aucun mot suite à nettoyage
questions = questions[questions['Cleaned_Title_Body'] != '']

4. Export CSV

In [47]:
questions.drop(columns=['Title', 'Body'], inplace=True)
In [48]:
questions.isna().sum()
Out[48]:
Id                    0
Tags                  0
Title_Body            0
Cleaned_Title_Body    0
dtype: int64
In [49]:
questions.to_csv('src/cleaned_questions.csv', index=False)
In [50]:
end_notebook = time.time()
exec_time(start_notebook, end_notebook)
Out[50]:
'00:27:02'
In [51]:
questions[questions.Id == 31246192]
Out[51]:
Id Tags Title_Body Cleaned_Title_Body
45466 31246192 nan How to check for NaN in golang <p>How can I ch... check nan check float variable nan e.g.

Le tags ci-dessus est nan. Lors de l'importation du fichier dans le prochain notebook, celui-ci sera reconnu comme NaN. Il sera à corriger